Skip to content

CREST Adapter#807

Open
calvinp0 wants to merge 187 commits into
mainfrom
crest_adapter
Open

CREST Adapter#807
calvinp0 wants to merge 187 commits into
mainfrom
crest_adapter

Conversation

@calvinp0

@calvinp0 calvinp0 commented Nov 27, 2025

Copy link
Copy Markdown
Member

Addition of CREST Adapter that complements the heuristic adapter.

This pull request adds support for the CREST conformer and transition state (TS) search method to the ARC project, along with several related improvements and code cleanups. The most important changes include integrating CREST as a TS search adapter, updating configuration and constants, and enhancing the heuristics TS search logic for better provenance tracking and code clarity.

CREST Integration:

  • Added CREST as a supported TS search method: updated JobEnum (arc/job/adapter.py), included CREST in the list of adapters and RMG family mapping, and registered it as a default incore adapter (arc/job/adapters/common.py, arc/job/adapters/ts/__init__.py). [1] [2] [3] [4]
  • Implemented a new test suite for CREST input generation (arc/job/adapters/ts/crest_test.py).
  • Added a Makefile target and installation script for CREST (Makefile). [1] [2]

Constants and Configuration:

  • Added the angstrom_to_bohr conversion constant to both Cython and Python constants modules (arc/constants.pxd, arc/constants.py). [1] [2] [3]

Heuristics TS Search Enhancements and Refactoring:

  • Refactored heuristics TS search logic to track and combine method provenance for TS guesses, allowing for more precise attribution when multiple methods contribute to a guess (arc/job/adapters/ts/heuristics.py). [1] [2] [3] [4]
  • Improved code readability and maintainability by reformatting imports and function calls, and clarifying data structures and comments in heuristics TS search (arc/job/adapters/ts/heuristics.py). [1] [2] [3] [4] [5] [6] [7]
    .

Comment thread arc/job/adapters/ts/crest.py Fixed
Comment thread arc/job/adapters/ts/crest.py Fixed
Comment thread arc/job/adapters/ts/crest.py Fixed
Comment thread arc/settings/settings.py Fixed
Comment thread arc/settings/settings.py Fixed
Comment thread arc/job/adapters/ts/autotst_ts.py Fixed
Comment thread arc/job/adapters/ts/crest.py Fixed
Comment thread arc/job/adapters/ts/heuristics.py Fixed
Comment thread arc/job/adapters/ts/heuristics.py Fixed
Comment thread arc/job/adapters/ts/heuristics.py Fixed
Comment thread arc/job/adapters/ts/crest.py Fixed

This comment was marked as resolved.

Comment thread arc/job/adapters/ts/crest.py Fixed

This comment was marked as resolved.

Comment thread arc/job/adapters/ts/heuristics.py Fixed
Comment thread arc/settings/settings.py Fixed
Comment thread arc/settings/settings.py Fixed
@calvinp0 calvinp0 force-pushed the crest_adapter branch 2 times, most recently from 3e88c36 to 7674f5f Compare February 2, 2026 09:49
@calvinp0 calvinp0 requested a review from Copilot February 2, 2026 12:10

This comment was marked as resolved.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

This comment was marked as resolved.

@calvinp0 calvinp0 force-pushed the crest_adapter branch 2 times, most recently from 9be935e to eab7647 Compare February 4, 2026 20:14
@calvinp0 calvinp0 requested a review from alongd February 4, 2026 21:30
Comment thread arc/settings/settings.py Fixed
calvinp0 added 10 commits July 8, 2026 07:13
When a TS did not converge, its spawned IRC endpoint species (e.g. IRC_TS0_1) is deleted.
The scheduler removed it from self.output and self.species_dict, but removed it from
self.species_list via reassignment (self.species_list = [...]), which rebinds only the
scheduler's variable and leaves the deleted species dangling in ARC.species (the same list
object, mutated in-place by the earlier append). save_project_info_file then iterates
ARC.species and looks each up in the synced self.output, raising KeyError and crashing the
run at the final summary step after all work was done.

Fix: (1) delete in-place via slice assignment so removals propagate through the shared list
like the appends do; (2) guard the summary loop to skip any species missing from self.output
(deleted IRC species shouldn't be reported anyway). Adds a regression test.
Follow-up to the IRC-species KeyError fix: save_project_info_file has a SECOND loop (building
<project>_info.yml for T3) with the same unguarded self.output[species.label] lookup, so a
dangling deleted IRC species would KeyError there as well once the first loop was guarded. Guard
it identically (skip species not in self.output).

Also rewrite the regression test to exercise save_project_info_file on a bare ARC instance
(ARC.__new__ + SimpleNamespace stand-ins) instead of constructing a full ARC object. The fix is
pure bookkeeping and does not need the Arkane BAC tables / molecule perception that a full ARC
build pulls in; the test now runs in ~1s instead of minutes and has no RMG dependency.
…actions

For reactions where a reactant participates more than once (e.g. OH + OH -> H2O + O), r_species is
deduplicated, which broke the normal-mode-displacement check two ways:

1. n_expected was sum(number_of_atoms for r_species) = the DEDUPED atom count (2 for OH+OH), so it
   never equaled the real TS atom count (4) and analyze_ts_normal_mode_displacement returned False
   before even looking at the mode. Count each species by its occurrence via get_species_count
   (the same dedup-aware pattern get_reactants_xyz already uses).

2. find_equivalent_atoms only found equivalence WITHIN a molecule, so it missed the cross-molecule
   equivalence between identical copies (the two O's, the two H's of OH+OH). The atom map can assign
   the reactive atom to one copy while the located TS uses the equivalent atom of the other copy;
   without the equivalence NMD cannot try the correct mapping. Added
   get_repeated_species_atom_equivalences: for a repeated species, atoms at the same position across
   copies are equivalent.

Validated on the real OH+OH -> H2O+O saddle (imag -768 cm-1) from the reproducibility runs: NMD now
returns True (was False). Recovers reaction 05 and other same-species H_Abstractions. Continuation of
the repeated-reactant dedup work (get_reactants_xyz / autotst reverse fix).
…A NEB level

Two issues surfaced running the QST2 adapter (reaction 21, intra_halogen_migration):

1. process_completed_tsg_queue_jobs hard-coded method='orca_neb' for every .log queue TS-guess
   job. Now that more than one queue adapter emits a .log (orca_neb and qst2), QST2's guess was
   collected but mislabeled 'orca_neb'. Thread the actual adapter through: the method now takes a
   `method` argument and the scheduler passes job.job_adapter.

2. orca_neb_settings['level'] used 'wb97xd/def2tzvp', but ORCA rejects Gaussian's 'wb97xd' spelling
   ("UNRECOGNIZED KEYWORD WB97XD"), so every NEB job errored and produced no geometry to parse
   ("Fallback parsing failed"). Use the ORCA spelling 'wb97x-d3/def2tzvp'.

Adds a regression test asserting a .log queue guess is attributed to 'qst2', not 'orca_neb'.
QST2 was generating its guess at wb97xd/def2tzvp, but ARC re-optimizes every TS guess at the
ts_guesses level (wb97xd/def2svp) in conf_opt before the final project-level opt. Doing the QST2
opt=qst2 at the larger def2tzvp basis was wasteful (its precision is discarded by the def2svp
conf_opt). Match the guess level to the TS-guess opt level.
Comment thread arc/settings/settings.py
'level': 'wb97x-d3/def2tzvp', # ORCA spelling; it does not accept Gaussian's 'wb97xd'
}

qst2_settings = {'maxcycle': 150,

Check notice

Code scanning / CodeQL

Unused global variable Note

The global variable 'qst2_settings' is not used.

Copilot Autofix

AI about 9 hours ago

To fix this without changing functionality, rename the variable to an intentionally-unused name that CodeQL recognizes, while preserving the dictionary value and comments.

Best single change in arc/settings/settings.py:

  • Change qst2_settings = {...} to _unused_qst2_settings = {...} at the shown block around line 273.
  • No imports, methods, or additional definitions are needed.

This keeps runtime behavior equivalent for this module snippet and satisfies the “intentionally unused” naming convention accepted by the query.

Suggested changeset 1
arc/settings/settings.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/arc/settings/settings.py b/arc/settings/settings.py
--- a/arc/settings/settings.py
+++ b/arc/settings/settings.py
@@ -270,9 +270,9 @@
                     'level': 'wb97x-d3/def2tzvp',  # ORCA spelling; it does not accept Gaussian's 'wb97xd'
                     }
 
-qst2_settings = {'maxcycle': 150,
-                 'level': 'wb97xd/def2svp',  # match the TS-guess opt level; the guess is refined later
-                 }
+_unused_qst2_settings = {'maxcycle': 150,
+                         'level': 'wb97xd/def2svp',  # match the TS-guess opt level; the guess is refined later
+                         }
 
 ase_default_options_dict = {'optimizer': 'BFGS',
                             'fmax': 0.001,
EOF
@@ -270,9 +270,9 @@
'level': 'wb97x-d3/def2tzvp', # ORCA spelling; it does not accept Gaussian's 'wb97xd'
}

qst2_settings = {'maxcycle': 150,
'level': 'wb97xd/def2svp', # match the TS-guess opt level; the guess is refined later
}
_unused_qst2_settings = {'maxcycle': 150,
'level': 'wb97xd/def2svp', # match the TS-guess opt level; the guess is refined later
}

ase_default_options_dict = {'optimizer': 'BFGS',
'fmax': 0.001,
Copilot is powered by AI and may make mistakes. Always verify output.
calvinp0 added 18 commits July 8, 2026 09:28
When Arkane emits errors but still produces output.py (e.g. a first attempt that failed on the
Eckart submerged-barrier ValueError and was then retried with tunneling off), run_arkane dumped the
entire multi-line Python traceback into arc.log at WARNING. Log the salient exception line plus a
count instead ("ValueError: ... [+N more stderr line(s)]"), preferring the real Python exception
over the conda wrapper's non-zero-exit line. Same condensing applied to the genuine-failure ERROR
path (full traceback remains in the run dir's stderr.log). Adds tests.
The SCF quadratic-convergence remedy was guarded on the token 'no_qc',
which is never appended to ess_trsh_methods; the token actually recorded
by trsh_keyword_no_qc() (and consumed by the Gaussian adapter) is 'no_xqc'.
As a result, after an l508 failure dropped 'scf=(qc)' and recorded 'no_xqc',
the next SCF failure re-added 'scf=(qc)', the next l508 failure dropped it
and appended 'no_xqc' again, and so on - wasting the retry budget and never
reaching the rest of the SCF ladder (NDamp, NoDIIS, guess=INDO, Fermi, ...).

- Align the three 'no_qc' guards in trsh.py to 'no_xqc'.
- Count a dropped 'scf=(qc)' (recorded as 'no_xqc') as attempted for the
  Fermi/Noincfock/NoVarAcc last-resort gate, which previously required
  'scf=(qc)' to still be present and was unreachable on the no_xqc path.
- Make the 'no_xqc' append idempotent and fix the copy-paste docstring of
  trsh_keyword_no_qc().
- Gaussian adapter: block the qc->xqc upgrade based on ess_trsh_methods;
  the old check ('no_xqc' in self.args['trsh'].values()) can never match
  the scheduler-built args shape {'trsh': [keyword_list]}.
- Add a retry-cycle simulation test (trsh_test) and adapter input tests
  (gaussian_test) that fail on the old behavior.
…g, pin ladders

Holistic review cleanup of the Gaussian ESS troubleshooting cycle (no behavior change):

- trsh_keyword_inaccurate_quadrature: remove the final elif branch. It guarded on
  'int=ultrafine', a token never added to ess_trsh_methods anywhere, so the guard was
  always true; the branch never advanced ess_trsh_methods and only re-appended already-
  present trsh keywords (deduped downstream by combine_parameters), firing solely on the
  terminal retry that trsh_ess_job discards via 'all_attempted'. Replaced with an accurate
  comment. Proven behavior-preserving by the new ladder test (passes pre- and post-removal).
- trsh_keyword_unconverged: correct the copy-paste docstring ('change of scf' -> fine-grid
  remedy for a generic l9999 Unconverged failure).
- Add regression tests pinning the InaccurateQuadrature and MaxOptCycles remedy ladders
  (escalation order + termination), which also document current behavior for the review.
trsh_keyword_intaccuracy fired on every Gaussian error (no error-class guard), so
input/method errors that a resubmit cannot fix (Syntax, InputError, ZMat, MP2,
OptOrientation, Scratch, l401 BasisSet-projection) each picked up a spurious
int=(Acc2E=14) step and burned a resubmit before hitting all_attempted.

Add GAUSSIAN_NON_RETRYABLE_KEYWORDS and a refusal branch in the shared if/elif chain
so these classes set couldnt_trsh=True immediately with a clear error, never entering
the helper cascade. Deliberately conservative: SCF, opt-cycles, internal-coordinate,
negative-eigenvalue, memory, checkfile and the generic 'Unknown' (scheduler retries
on another node) classes are untouched.

Test asserts the seven non-retryable classes refuse with no remedy appended, and that
SCF and MaxOptCycles still receive their remedies unchanged.
The adapter only emitted integral_algorithm under 'if self.fine', so a non-fine opt (or
IRC) whose only troubleshooting remedy was int=(Acc2E=14) produced a route byte-identical
to the un-troubleshot one - a guaranteed no-op resubmit. Capture acc2e_requested and, for
non-fine opt/IRC jobs, emit a bare integral=(Acc2E=14) via the route's fine slot so the
tightened integral accuracy actually takes effect. Fine jobs still fold Acc2E into the
ultrafine grid; normal non-fine jobs (no Acc2E trsh) are unchanged.

Render tests cover: non-fine opt emits it, non-fine opt without the trsh is unchanged,
fine opt still folds into ultrafine, and non-fine IRC emits it.
Rework the MaxOptCycles remedy ladder so it recomputes the Hessian before flipping the
step algorithm, and is transition-state aware:

    maxcycle=200 -> recalcfc=5 -> calcall -> RFO -> [GDIIS -> GEDIIS, minimization only]

Recomputing force constants (recalcfc every 5 steps, then calcall every step) is the
first-line remedy for a stuck optimization and is essential for a TS opt, where following
the single negative eigenvalue depends on Hessian quality. GDIIS/GEDIIS are minimization-
oriented DIIS step accelerators that can walk a TS opt downhill into a nearby minimum and
lose the saddle, so they are skipped when is_ts is True (RFO/eigenvector-following is kept).

- Thread is_ts from the scheduler (species.is_ts) through trsh_ess_job into
  trsh_keyword_opt_maxcycles and prioritize_opt_methods.
- prioritize_opt_methods: keep a single step algorithm (TS -> RFO only) and collapse the
  force-constant family to the most aggressive directive (calcall > recalcfc=* > calcfc).
- Gaussian adapter: drop the base-route calcfc when the ladder supplies recalcfc/calcall,
  so the opt=() clause never carries conflicting Hessian options.

Tests: new min/TS ladder tests + prioritize unit test (trsh_test); adapter FC-resolution
and TS-route render tests (gaussian_test). Updated the pre-existing test_trsh_ess_job
step-ladder and the job_23/job_24 route fixtures to the new ladder (routes verified by
rendering). Note: gaussian_test::test_trsh_write_input_file has a pre-existing content_21
(InaccurateQuadrature) failure identical on the parent branch, unrelated to this change.
…e non-retryable, P2 Acc2E non-fine, P3 TS-aware Hessian ladder)
…rom_dict

ARCSpecies.from_dict (the path main.py uses to load input.yml species) builds self.mol from a
spin-agnostic SMILES/InChI (Molecule(smiles=...)), which RMG perceives at a default spin state, but
never reconciled it to the declared multiplicity -- the reconciliation only ran when multiplicity was
None. So a singlet carbene [CH2] with multiplicity: 1 ended up with a TRIPLET .mol (u2, mult 3) while
self.multiplicity was 1. Downstream graph-based logic (RMG family determination, 3D<->2D isomorphism
checks) then saw a triplet: RMG's 1+2_Cycloaddition family requires a singlet carbene, so it failed to
match -> family=None -> the reaction fell back to ts_adapters_for_unknown_unimolecular=[linear] and was
silently under-served (reaction_01). The kwargs __init__ path already reconciles; from_dict did not.

Fix: mirror the kwargs path -- call reconcile_mol_multiplicity() in from_dict when the mol was built
from a spin-agnostic descriptor (smiles/inchi, not adjlist/mol), an explicit multiplicity is given and
disagrees with the perceived mol, and number_of_radicals is not declared (which would mark an
intentional open-shell state). reconcile_mol_multiplicity() only pairs same-atom radicals into a lone
pair and no-ops otherwise, so triplet carbene, O2, OH, and closed-shell species are untouched.

Validated: from_dict [CH2] mult 1 -> C u0 p1; reaction C=C + [CH2](m1) -> cyclopropane now resolves to
family 1+2_Cycloaddition (was None), so it gets kinbot/xtb_gsm/orca_neb/linear instead of linear-only.
General fix for any spin-ambiguous species (singlet carbenes, etc.) loaded from an input file.
…-derived bonds

The reaction atom map is only determined up to permutations of chemically
equivalent atoms. For reaction CCO[O] <=> C=C + [O]O (HO2 elimination,
benchmark reaction 04) the map assigns the heavy atoms and the transferring
beta-H correctly, but crosses the four spectator H atoms between the two
carbons of the ethylene product (all four H positions there are equivalent,
so the map is chemically valid). Pulling product bonds back through such a
permuted map inflates the formed/broken sets to 5 formed + 6 broken bonds
(instead of 1 formed + 2 broken + 1 changed), and NMD then falsely rejected
the genuine concerted-elimination saddle (imag ~-1074 1/cm) because the
spurious spectator C-H 'reactive' bonds show no motion along the mode.

Fix: derive candidate reactive-bond sets in NMD via the new
get_bond_change_candidates(), which tries the RMG-family-recipe-derived
bonds (canonical, atom-map independent; the path already existed for the
atom_map-is-None case) first, keeping the atom-map-derived sets as a
fallback candidate. Renamed ARCReaction._get_reactive_bonds_from_family to
the public get_reactive_bonds_from_family so NMD can use it.

Verified against the real benchmark saddles: the -1074 saddle now passes
NMD while the spurious -2144, -238, and -531 saddles of the same TS search
still fail; all existing NMD fixtures classify unchanged.
A conf_opt that drove atoms collinear during optimization hit Gaussian L716
('Angle in z-matrix outside the allowed range 0 < x < 180', keyword 'ZMat') and
was abandoned immediately, because 'ZMat' was in GAUSSIAN_NON_RETRYABLE_KEYWORDS.
But ARC always submits Cartesian geometries, so a ZMat error is never a bad input -
it is the textbook collinear-angle degeneracy that Cartesian optimization sidesteps.

- Remove 'ZMat' from GAUSSIAN_NON_RETRYABLE_KEYWORDS (Syntax/InputError/MP2/
  OptOrientation/Scratch/BasisSet remain genuine dead ends).
- Make trsh_keyword_cartesian fire on 'ZMat' as well as 'InternalCoordinateError'
  (L103), so a ZMat error records 'cartesian' and emits opt=(cartesian) on the retry.
- Termination is preserved: opt=(cartesian) is tried once (guarded by 'cartesian'
  not in ess_trsh_methods); a recurring ZMat leaves ess_trsh_methods unchanged, so
  trsh_ess_job terminates via 'all_attempted'. Verified for opt and conf_opt.

Before: ZMat -> immediate refusal (couldnt_trsh=True, ess=[]).
After:  ZMat -> opt=(cartesian) resubmit, then terminate if it recurs.

Tests: ZMat yields opt=(cartesian) and is not refused; a second ZMat after cartesian
terminates (opt and conf_opt); the other non-retryable classes still refuse.
…, TSs)

Previously any species with >1 atom received guess=mix on fresh jobs
regardless of electronic structure. guess=mix seeds a broken-symmetry
initial guess, which is only meaningful for open-shell (biradical)
singlets and TS optimizations; on restricted closed-shell routes it
merely perturbs the initial guess (Gaussian still applies the HOMO/LUMO
mixing but the R wavefunction cannot break spin symmetry), and simple
high-spin radicals converge to the correct state from the default guess.

Benchmark evidence (arcbench, 911 Gaussian jobs): doublets seeded with
guess=mix show <S**2> = 0.7524-0.7787 (mean 0.7555) before annihilation,
statistically identical to guess=read-seeded doublets, so dropping mix
for them is safe; all 431 singlet jobs ran restricted and converged.

guess=read (checkfile) and guess=INDO (troubleshooting) precedence is
unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants